package com.itgacl.magic4j.libcommon.util.geohash;


import ch.hsr.geohash.BoundingBox;
import ch.hsr.geohash.GeoHash;
import ch.hsr.geohash.WGS84Point;
import ch.hsr.geohash.util.BoundingBoxGeoHashIterator;
import ch.hsr.geohash.util.TwoGeoHashBoundingBox;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.awt.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
 * 根据一个经纬度坐标、距离然后求另外一个经纬度坐标的作用主要就是确定一个最小外包矩形(Minimum bounding rectangle，简称MBR)。
 * 例如，我要找一个坐标点(lat,lon)的5公里范围内的所有商户信息、景点信息等。这个MBR就是一个最大的范围，这个矩形是包含5公里范围内所有这些有效信息的一个最小矩形。
 * 利用公式，求出四个方向0度、90度、180度、270度方向上的四个坐标点就可以得到这个MBR。
 * 参考：https://segmentfault.com/a/1190000000624088
 */
public class GeoHashUtil {


    public static final double Ea = 6378137;     //赤道半径
    public static final double Eb = 6356725;     //极半径
    public static final double Max_Lat = 90;
    public static final double Min_Lat = -90;
    public static final double Max_Lng = 180;
    public static final double Min_Lng = -180;
    private static final int length = 20;
    private static final double latUnit = (Max_Lat - Min_Lat) / (1 << 20);
    private static final double lngUnit = (Max_Lng - Min_Lng) / (1 << 20);
    private static final String[] base32Lookup =
            {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "b", "c", "d", "e", "f", "g", "h",
                    "j", "k", "m", "n", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"};

    private static void convert(double min, double max, double value, List<Character> list) {
        if (list.size() > (length - 1)) {
            return;
        }
        double mid = (max + min) / 2;
        if (value < mid) {
            list.add('0');
            convert(min, mid, value, list);
        } else {
            list.add('1');
            convert(mid, max, value, list);
        }
    }

    private static String base32Encode(final String str) {
        String unit = "";
        StringBuilder sb = new StringBuilder();
        for (int start = 0; start < str.length(); start = start + 5) {
            unit = str.substring(start, start + 5);
            sb.append(base32Lookup[convertToIndex(unit)]);
        }
        return sb.toString();
    }

    private static int convertToIndex(String str) {
        int length = str.length();
        int result = 0;
        for (int index = 0; index < length; index++) {
            result += str.charAt(index) == '0' ? 0 : 1 << (length - 1 - index);
        }
        return result;
    }

    /**
     * 获取经纬度的GeoHash值
     * @param lat 纬度
     * @param lng 经度
     * @return
     */
    public static String getGeoHashVal(double lat, double lng) {
        List<Character> latList = new ArrayList<>();
        List<Character> lngList = new ArrayList<>();
        convert(Min_Lat, Max_Lat, lat, latList);
        convert(Min_Lng, Max_Lng, lng, lngList);
        StringBuilder sb = new StringBuilder();
        for (int index = 0; index < latList.size(); index++) {
            sb.append(lngList.get(index)).append(latList.get(index));
        }
        return base32Encode(sb.toString());
    }

    /**
     * 获取经纬度周边的GeoHash值列表
     * @param lat 纬度
     * @param lng 经度
     * @return
     */
    public static List<String> getAroundGeoHashVal(double lat, double lng) {
        List<String> list = new ArrayList<>();
        list.add(getGeoHashVal(lat, lng));
        list.add(getGeoHashVal(lat + latUnit, lng));
        list.add(getGeoHashVal(lat - latUnit, lng));
        list.add(getGeoHashVal(lat, lng + lngUnit));
        list.add(getGeoHashVal(lat, lng - lngUnit));
        list.add(getGeoHashVal(lat + latUnit, lng + lngUnit));
        list.add(getGeoHashVal(lat + latUnit, lng - lngUnit));
        list.add(getGeoHashVal(lat - latUnit, lng + lngUnit));
        list.add(getGeoHashVal(lat - latUnit, lng - lngUnit));
        return list;
    }

    /**
     * @param maxLat    矩形内纬度的最大值
     * @param minLng    矩形内经度的最小值
     *                  <p>
     *                  左上角的经纬度
     * @param minLat    矩形内纬度的最小值
     * @param maxLng    矩形内经度的最大值
     *                  <p>
     *                  右下角的经纬度
     * @param precision geoHash的精度
     * @Description: 获取指定经纬度范围内的geoHash编码
     */

    public static HashSet<String> getGeoHashByFence(double maxLat, double minLng, double minLat, double maxLng, int precision) {
        //通过矩形的左下角 (西南角) 构建一个精度为precision的geoHash值
        GeoHash southWestCorner = GeoHash.withCharacterPrecision(minLat, minLng, precision);
        //通过矩形的右上角 (东北角) 构建一个精度为precision的geoHash值
        GeoHash northEastCorner = GeoHash.withCharacterPrecision(maxLat, maxLng, precision);
        //使用两个geoHash构建一个外接盒型
        TwoGeoHashBoundingBox twoGeoHashBoundingBox = new TwoGeoHashBoundingBox(southWestCorner, northEastCorner);
        //盒型geoHash迭代器,获取矩形内的全部geoHash
        BoundingBoxGeoHashIterator iterator = new BoundingBoxGeoHashIterator(twoGeoHashBoundingBox);
        List<Rectangle> list = Lists.newArrayList();
        HashSet<String> set = Sets.newHashSet();
        GeoHash geoHash;
        //循环遍历
        while (iterator.hasNext()) {
            geoHash = iterator.next();
            list.add(getFence(geoHash));
            //获取geoHash编码
            set.add(geoHash.toBase32());
        }
        return set;
    }

    /**
     * @param geoHash 指定的geoHash
     * @Description: 获取指定geoHash的矩形
     */

    public static Rectangle getFence(GeoHash geoHash) {
        //获取geoHash的矩形
        BoundingBox boundingBox = geoHash.getBoundingBox();
        //获取矩形的左下角(西南角)经纬度
        WGS84Point northWestCorner = boundingBox.getSouthWestCorner();
        //获取矩形的右上角(东北角)经纬度
        WGS84Point southEastCorner = boundingBox.getNorthEastCorner();
        //包装成矩形
        return new Rectangle(Integer.getInteger(northWestCorner.getLongitude() + ""), Integer.getInteger(northWestCorner.getLatitude() + ""), Integer.getInteger(southEastCorner.getLongitude() + ""), Integer.getInteger(southEastCorner.getLatitude() + ""));
    }

    private static MBR.Point GetlatLon(double LAT, double LON, double distance, double angle) {
        double dx = distance * 1000 * Math.sin(angle * Math.PI / 180.0);
        double dy = distance * 1000 * Math.cos(angle * Math.PI / 180.0);
        double ec = Eb + (Ea - Eb) * (90.0 - LAT) / 90.0;
        double ed = ec * Math.cos(LAT * Math.PI / 180);
        double newLon = (dx / ed + LON * Math.PI / 180.0) * 180.0 / Math.PI;
        double newLat = (dy / ec + LAT * Math.PI / 180.0) * 180.0 / Math.PI;
        MBR.Point point = new MBR.Point();
        point.setLongitude(newLon);
        point.setLatitude(newLat);
        return point;
    }

    public static MBR GetRectRange(double centorlatitude, double centorLogitude, double distance) {

        MBR.Point point1 = GetlatLon(centorlatitude, centorLogitude, distance, 0);
        MBR.Point point2 = GetlatLon(centorlatitude, centorLogitude, distance, 180);
        MBR.Point point3 = GetlatLon(centorlatitude, centorLogitude, distance, 90);
        MBR.Point point4 = GetlatLon(centorlatitude, centorLogitude, distance, 270);

        MBR mbr = new MBR();
        mbr.setMaxLatitude(point1.getLatitude());
        mbr.setMinLatitude(point2.getLatitude());
        mbr.setMaxLongitude(point3.getLongitude());
        mbr.setMinLongitude(point4.getLongitude());
        return mbr;
    }

    public static MBR GetRectRange2(double centorlatitude, double centorLogitude, double distance) {
        MBR.Point point1 = GetNewLatLon(centorlatitude, centorLogitude, distance, 0);
        MBR.Point point2 = GetNewLatLon(centorlatitude, centorLogitude, distance, 180);
        MBR.Point point3 = GetNewLatLon(centorlatitude, centorLogitude, distance, 90);
        MBR.Point point4 = GetNewLatLon(centorlatitude, centorLogitude, distance, 270);
        MBR mbr = new MBR();
        mbr.setMaxLatitude(point1.getLatitude());
        mbr.setMinLatitude(point2.getLatitude());
        mbr.setMaxLongitude(point3.getLongitude());
        mbr.setMinLongitude(point4.getLongitude());
        return mbr;
    }

    private static MBR.Point GetNewLatLon(double lat, double lon, double d, double bearing) {
        double R = 6378.137;
        double φ1 = ConvertDegreesToRadians(lat);
        double λ1 = ConvertDegreesToRadians(lon);
        double θ = ConvertDegreesToRadians(bearing);
        double φ2 = Math.asin(Math.sin(φ1) * Math.cos(d / R) + Math.cos(φ1) * Math.sin(d / R) * Math.cos(θ));
        double λ2 = λ1 + Math.atan2(Math.sin(θ) * Math.sin(d / R) * Math.cos(φ1), Math.cos(d / R) - Math.sin(φ1) * Math.sin(φ2));
        λ2 = (λ2 + 3 * Math.PI) % (2 * Math.PI) - Math.PI; // normalise to -180..+180°
        double newLat = ConvertRadiansToDegrees(φ2);
        double newLon = ConvertRadiansToDegrees(λ2);
        MBR.Point point = new MBR.Point();
        point.setLongitude(newLon);
        point.setLatitude(newLat);
        return point;
    }

    public static double ConvertDegreesToRadians(double degrees) {
        return degrees * Math.PI / 180;
    }


    public static double ConvertRadiansToDegrees(double radian) {
        return radian * 180.0 / Math.PI;
    }
}
